<!-- Card1 [日-中] 正面 -->

{{^Alt1}}

<main id="FrontSide" class="CardSide">
  <div class="Top">
    <span class="Level">{{Tags}}</span>
    <a target="_blank" class="Feedback">反馈</a>
  </div>

  <header class="Question">
    <div class="VocabAudio">{{VocabAudio}}</div>
    <h1 class="VocabKanji">
      <span lang="ja">{{furigana:VocabKanji}}</span>
      <span class="VocabPitch">{{VocabPitch}}</span>
    </h1>
    <a target="_blank" class="Search">🔍</a>
  </header>

  <ul class="SentenceList">
    {{#SentKanji1}}
    <li class="Sentence">
      <h3 class="SentKanji LabelIndent" lang="ja">
        {{#SentFurigana1}}{{kanji:SentFurigana1}}{{/SentFurigana1}}
        {{^SentFurigana1}}{{kanji:SentKanji1}}{{/SentFurigana1}}
      </h3>
    </li>
    {{/SentKanji1}}

    {{#SentKanji2}}
    <li class="Sentence">
      <h3 class="SentKanji LabelIndent" lang="ja">
        {{#SentFurigana2}}{{kanji:SentFurigana2}}{{/SentFurigana2}}
        {{^SentFurigana2}}{{kanji:SentKanji2}}{{/SentFurigana2}}
      </h3>
    </li>
    {{/SentKanji2}}

    {{#SentKanji3}}
    <li class="Sentence">
      <h3 class="SentKanji LabelIndent" lang="ja">
        {{#SentFurigana3}}{{kanji:SentFurigana3}}{{/SentFurigana3}}
        {{^SentFurigana3}}{{kanji:SentKanji3}}{{/SentFurigana3}}
      </h3>
    </li>
    {{/SentKanji3}}

    {{#SentKanji4}}
    <li class="Sentence">
      <h3 class="SentKanji LabelIndent" lang="ja">
        {{#SentFurigana4}}{{kanji:SentFurigana4}}{{/SentFurigana4}}
        {{^SentFurigana4}}{{kanji:SentKanji4}}{{/SentFurigana4}}
      </h3>
    </li>
    {{/SentKanji4}}
  </ul>
</main>

<script>
  function CONFIG() {
    // --- 以下为设置里的预设项，根据个人需求调整 --- //
    const settings = {
      lang: 'zh-Hans',      // 显示语言: 'zh-Hans' 简体中文 | 'zh-Hant' 繁体中文
      dict: {
        ios: 'moji',        // 在 iOS 使用 MOji 辞書
        android: 'eudic',    // 在 Android 使用 MOji 辞書
        mac: 'dict',        // 在 macOS 使用系统自带的词典
        win: 'goldendict',  // 在 Windows 使用 GoldenDict-ng 词典
        other: 'weblio',    // 在其他平台使用 Weblio 国語辞典
      },
      display: 'default',   // 正面单词显示: 'default' 显示汉字和注音假名 | 'kana' 只显示假名 | 'kanji' 只显示汉字
      playback: 'force',    // 背面播放设置: 'default' 跟随牌组的系统设置 | 'force' 强制只播放单词音频
      autoCopy: {           // 背面自动复制: true 开启 | false 关闭
        ipad: true,
        iphone: false,
        ankiweb: false,
      },
      tts: {
        enable: 'fallback', // 在线 TTS 开关: 'always' 始终开启 | 'fallback' 无本地音频时启用 | 'never' 始终关闭
        hotkey: 'G',        // 播放快捷键，仅支持桌面端
        domain: [
          // 可以添加更多域名，按顺序依次尝试播放
          'https://anki.0w0.live/',
          'https://ms-ra-forwarder-for-ifreetime-v9q1.vercel.app/',
        ],
        params: {
          voiceName: 'ja-JP-KeitaNeural,ja-JP-NanamiNeural', // 语音为多个时随机选择一个
          speed: -4, // 语速范围 -50 到 100，0 为正常语速
        }
      }
    }
    // --- 以下为词典预设项，新增词典参考格式添加 --- //
    const dictOptions = {
      'dict': 'dict://{query}',                                     // macOS 词典 [mac]
      'eudic': 'eudic://dict/{query}',                              // Eudic 欧路词典 [win|mac|ios|android]
      'goldendict': 'goldendict://{query}',                         // GoldenDict-ng 词典 [win]
      'dicttango': 'dttp://app.dicttango/WordLookup?word={query}',  // DictTango [android]
      'monokakido': 'mkdictionaries:///?text={query}',              // Monokakido 物書堂 [mac|ios]
      'moji': 'mojisho://?search={query}',                          // MOJi 辞書 [ios|android]
      'mojidict': 'https://www.mojidict.com/searchText/{query}',    // MOJi 辞書网页版 [web]
      'google': 'https://www.google.com/search?q={query}',          // Google Search [web]
      'weblio': 'https://www.weblio.jp/content/{query}',            // Weblio 国語辞典 [web]
      'weblioCJJC': 'https://cjjc.weblio.jp/content/{query}',       // Weblio 日中中日 [web]
      'takoboto': 'https://takoboto.jp/?q={query}',                 // Takoboto [web]
      'mazii': 'https://mazii.net/zh-TW/search/word/jatw/{query}',  // Mazii [web]
      'jisho': 'https://jisho.org/search/{query}',                  // Jisho [web]
      'kotobank': 'https://kotobank.jp/search?q={query}',           // Kotobank [web]
      'goo': 'https://dictionary.goo.ne.jp/srch/all/{query}/m0u/',  // Goo 辞書 [web]
    }

    return { settings, dictOptions }
  }

  function getTranslation(key) {
    const translations = {
      feedback: {
        'zh-Hans': '反馈',
        'zh-Hant': '反饋',
      },
      dictionaryTip: {
        'zh-Hans': '未配置适用于此平台的词典',
        'zh-Hant': '未配置適用於此平台的詞典',
      },
      updateReminder: {
        'zh-Hans': '请更新至最新版后再提交反馈',
        'zh-Hant': '請更新至最新版後再提交反饋',
      },
      versionCurrent: {
        'zh-Hans': '当前版本',
        'zh-Hant': '當前版本',
      },
      versionLatest: {
        'zh-Hans': '最新版本',
        'zh-Hant': '最新版本',
      },
      updateDetails: {
        'zh-Hans': '更新说明',
        'zh-Hant': '更新說明',
      },
      cardFeedback: {
        'zh-Hans': '卡片反馈',
        'zh-Hant': '卡片反饋',
      },
      cardInfo: {
        'zh-Hans': '卡片信息',
        'zh-Hant': '卡片資訊',
      },
      cardVersion: {
        'zh-Hans': '卡片版本',
        'zh-Hant': '卡片版本',
      },
      feedbackContent: {
        'zh-Hans': '反馈内容',
        'zh-Hant': '反饋內容',
      }
    }
    const lang = CONFIG().settings.lang
    return translations[key]?.[lang] || ''
  }

  function getToday() {
    return new Date().toLocaleDateString('sv-SE')
  }

  function setStorage(value) {
    Object.keys(localStorage)
      .filter(key => key.startsWith('JLPT_'))
      .forEach(key => localStorage.removeItem(key))
    const key = `JLPT_${getToday()}`
    localStorage.setItem(key, JSON.stringify(value))
  }

  function getStorage() {
    const key = `JLPT_${getToday()}`
    return JSON.parse(localStorage.getItem(key)) || {}
  }

  function getCurrentVersion(tags = '{{Tags}}') {
    return tags.split(/\s+|::/).find(part => part.startsWith('v')) || ''
  }

  async function getLatestReleaseInfo() {
    let latest, info
    try {
      const response = await fetch('https://api.github.com/repos/5mdld/anki-jlpt-decks/releases/latest')
      if (!response.ok) throw new Error(response.status)

      const data = await response.json()
      latest = `v${data.tag_name?.split('_')[0].replace(/^v/, '')}`
      info = data.body?.replace(/`(.*?)`/g, (match, p1) => `<code>${p1}</code>`)
        .replace(/\r?\n/g, '<br>')
        .replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')
        .replace(/(\s+)-(?!(?:[^<]*>|[^<>]*<\/code>))/g, '$1◦')
        .replace(/-(?!(?:[^<]*>|[^<>]*<\/code>))/g, '▪')
    } catch (err) {
      console.error(err)
    }
    return { latest, info }
  }




  function createDialog(config = {}) {
    if (document.getElementById('DynamicDialog')) {
      closeDialog()
    }
    const modal = document.createElement('div')
    modal.id = 'DynamicDialog'
    modal.className = 'DialogOverlay'
    modal.innerHTML = `
    <div class="DialogContent">
      <h2 class="DialogTitle">${config.title || '提示'}</h2>
      <div class="DialogBody">${config.content || ''}</div>
      <div class="DialogFooter">
        ${config.cancelText ? `<a target="_blank" class="DialogButton CancelButton">${config.cancelText}</a>` : ''}
        <a target="_blank" class="DialogButton ConfirmButton">${config.confirmText || '确定'}</a>
      </div>
    </div>
    `
    modal.querySelector('.DialogContent').addEventListener('click', e => e.stopPropagation())
    modal.querySelector('.CancelButton')?.addEventListener('click', config.onCancel || closeDialog)
    modal.querySelector('.ConfirmButton').addEventListener('click', config.onConfirm || closeDialog)
    modal.addEventListener('click', closeDialog)
    document.getElementById('qa').appendChild(modal)
    document.body.style.overflow = 'hidden'
  }

  function closeDialog() {
    document.getElementById('DynamicDialog')?.remove()
    document.body.style.overflow = ''
  }




  async function checkVersion(force = false) {
    const current = getCurrentVersion()
    const storage = getStorage()
    const el = document.querySelector('.Feedback')

    const shouldFetch = force || (current && !storage.latest?.startsWith?.('v'))
    const { latest, info } = shouldFetch ? await getLatestReleaseInfo().then(res => (setStorage(res), res)) : storage

    const icon = el.querySelector('i')
    if (!getTags().isDeleted && current && latest && current < latest) {
      if (!icon) el.insertAdjacentHTML('afterbegin', '<i>🎉 </i>')
    } else {
      icon?.remove()
    }
    return { current, latest, info }
  }





  function feedback(e) {
    const feedbackBtn = document.querySelector('.Feedback')
    feedbackBtn.addEventListener('click', async e => {
      const current = getCurrentVersion()
      const { latest, info } = await checkVersion(!current)

      if (!current || current < latest) {
        createDialog({
          title: `
          <p>🎉 可用更新</p>
          <span>${getTranslation('updateReminder')}</span>
          `,
          content: `
          <p>📌 ${getTranslation('versionCurrent')}: ${current || 'Unknown'}</p>
          <p>🌟 ${getTranslation('versionLatest')}: ${latest || 'Unknown'}</p>
          <p>🚀 ${getTranslation('updateDetails')}: </p>
          <ul>${info || '- No details available'}</ul>
          `,
          cancelText: '取消',
          confirmText: '更新',
          onConfirm: () => {
            document.querySelector('a.ConfirmButton').href = 'https://github.com/5mdld/anki-jlpt-decks/releases/latest'
            closeDialog()
          }
        })
      } else {
        feedbackBtn.href = getFeedbackLink()
      }
    })
  }

  function getFeedbackLink(platform = 'github', VocabKanji = '{{text:kanji:VocabKanji}}', NoteID = '{{text:NoteID}}') {
    const current = getCurrentVersion()
    const urls = {
      github: `https://github.com/5mdld/anki-jlpt-decks/issues/new?${Object.entries({
        title: `[${getTranslation('cardFeedback')}] ${getToday()} 「${VocabKanji}」`,
        body: `### ${getTranslation('cardInfo')}\n- ${getTranslation('cardVersion')}: ${current}\n- VocabKanji: ${VocabKanji}\n- NoteID: ${NoteID}\n\n### ${getTranslation('feedbackContent')}\n`
      }).map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')}`,
      feishu: `https://ncn8ci2h7v0y.feishu.cn/share/base/form/shrcnTh5DRxtrGWtiWTkdBlSWze?${new URLSearchParams({
        NoteID,
        Version: current
      })}`,
    }
    return urls[platform]
  }

  function getPlatform() {
    return ['ios', 'android', 'mac', 'win'].find(p => document.documentElement.className.includes(p)) || 'other'
  }

  function getDevice() {
    if (isAnkiWeb()) return 'ankiweb'
    return ['iphone', 'ipad'].find(p => document.documentElement.className.includes(p)) || 'other'
  }

  function isAndroid() {
    return !!document.documentElement.className.includes('android')
  }

  function isAnkiWeb() {
    return !!document.getElementById('quiz')
  }

  function isBackSide() {
    return !!document.getElementById('BackSide')
  }

  function cleanWord(word = '{{text:kanji:VocabKanji}}') {
    return word.replace(/\[[^\]]*\]|\([^)]*\)|[0-9!@#$%^&*()_+\-='":\\|,.<>/?~～〜\s]+/g, '')
  }

  function lookUp(word = '{{text:kanji:VocabKanji}}') {
    const searchBtn = document.querySelector('.Search')
    searchBtn.addEventListener('click', e => {
      const cleaned = cleanWord()
      const dict = CONFIG().settings.dict[getPlatform()]
      const scheme = CONFIG().dictOptions[dict]
      if (!scheme) {
        createDialog({
          content: `<p class="text-center">${getTranslation('dictionaryTip')}</p>`,
          cancelText: '取消',
          confirmText: '查看文档',
          onConfirm: () => {
            document.querySelector('a.ConfirmButton').href = 'https://github.com/5mdld/anki-jlpt-decks/blob/main/README.md'
            closeDialog()
          }
        })
      }
      searchBtn.href = scheme.replace('{query}', encodeURIComponent(cleaned))
    })
  }

  function forcePlayback() {
    if (CONFIG().settings.playback !== 'force') return
    const el = document.querySelector('.VocabAudio .replay-button')
    if (el) el.click()
  }

  function hideFrontElements() {
    document.querySelector('#FrontSide ul').style.display = 'none'
  }

  function hideFurigana() {
    if (CONFIG().settings.display === 'kanji') {
      document.querySelectorAll('.VocabKanji rt').forEach(rt => {
        rt.style.display = isBackSide() ? 'ruby-text' : 'none'
      })
    }
  }

  function hideKanji() {
    if (CONFIG().settings.display === 'kana') {
      if (isBackSide()) {
        if (isAndroid()) {
          updateText('.VocabKanji span[lang="ja"]', '{{furigana:VocabKanji}}')
        }
        return
      }
      const isKatakana = getTags().isLoanword || /^[ァ-ヴー]+$/.test('{{VocabKanji}}')
      updateText('.VocabKanji span[lang="ja"]', isKatakana ? '{{kanji:VocabKanji}}' : '{{VocabFurigana}}')
    }
  }

  function audioStylePatch() {
    const target = document.querySelector('#FrontSide .VocabAudio')
    const source = document.querySelector('#qa > .VocabAudio')
    if (source && target && !target.innerHTML.trim()) {
      source.classList.remove('!hidden')
      target.replaceWith(source)
    }
  }

  function updateText(selector, text) {
    if (!text) return
    const el = document.querySelector(selector)
    if (el) {
      isAndroid() || el.innerHTML.includes('<ruby>')
        ? el.innerHTML = text
        : el.textContent = text
    }
  }

  function getTags(tags = '{{Tags}}') {
    const ignoreTags = ['$', '^', 'v']
    const deleteTags = ['del', 'delete', 'remove', 'outdated']

    const rawTags = tags.split(/\s+/)
      .map(tag => tag.split(/::|-/).pop())
      .filter(tag => tag && !ignoreTags.some(prefix => tag.startsWith(prefix)))

    const deleted = rawTags.filter(tag => deleteTags.includes(tag))
    const normal = rawTags.filter(tag => !deleteTags.includes(tag))

    const compareTags = (a, b) => {
      const getPriority = s => s.startsWith('N') ? 0 : /^[a-zA-Z]/.test(s) ? 1 : 2
      const pa = getPriority(a)
      const pb = getPriority(b)
      if (pa !== pb) return pa - pb
      return a.localeCompare(b)
    }
    const sorted = [...normal].sort(compareTags)

    return {
      all: [...sorted, ...deleted],
      formatted: [...sorted, ...deleted].join('・'),
      isDeleted: deleted.length > 0,
      isLoanword: sorted.includes?.('外'),
    }
  }

  function setLang() {
    updateText('.Level', getTags().formatted)
    updateText('.Feedback', getCurrentVersion() ? `${getCurrentVersion()}・${getTranslation('feedback')}` : getTranslation('feedback'))
    const lang = CONFIG().settings.lang
    if (lang === 'zh-Hant') {
      document.documentElement.lang = lang
      updateText('.VocabDef', '{{VocabDefTC}}')
      const defs = ['{{SentDefTC1}}', '{{SentDefTC2}}', '{{SentDefTC3}}', '{{SentDefTC4}}']
      defs.forEach((def, index) => updateText(`li:nth-child(${index + 1}) .SentDef`, def))
    }
  }

  function removeSpaces() {
    document.querySelectorAll('.VocabPlus, .VocabPoS, .SentKanji, .SentFurigana, .SentDef').forEach(el => {
      el.innerHTML = el.innerHTML
        .replace(/\s*\n\s*/g, '')
        .replace(/>\s+</g, '><')
        .replace(/(<[^>]+>)|\s+/g, (m, tag) => tag || '')
    })
  }

  function toggleBlur() {
    document.querySelectorAll('.VocabKanji, .VocabFurigana, .VocabPlus, .SentKanji').forEach(el =>
      el.classList.toggle('blur')
    )
  }

  function getType(index) {
    return ['{{SentType1}}', '{{SentType2}}', '{{SentType3}}', '{{SentType4}}'][index] || '例'
  }

  function setType() {
    ['.SentKanji', '.SentFurigana', '.SentDef', '.VocabPlus', '.VocabPoS'].forEach(selector => {
      document.querySelectorAll(selector).forEach((el, i) => {
        if (!el.textContent.trim() || el.querySelector('em')) return
        const typeMap = {
          'SentDef': '［訳］',
          'VocabPlus': '［補］',
          'SentKanji': `［${getType(i)}］`,
          'SentFurigana': `［${getType(i)}］`,
          'VocabPoS': '{{VocabPoS}}' ? '［{{VocabPoS}}］' : '［名］',
        }
        const type = typeMap[Object.keys(typeMap).find(key => el.className.includes(key))]
        el.insertAdjacentHTML('afterbegin', `<em lang='ja'>${type}</em>`)
      })
    })
  }

  function markWords(word = '{{text:kanji:VocabKanji}}') {
    const wordRegex = /[一-龠々ヵヶ]+|[ぁ-んァ-ヴー]+/g
    const kanjiRegex = /[一-龠々ヵヶ]/
    const parts = cleanWord().match(wordRegex) ?? []
    const regexParts = parts.map(part => {
      return kanjiRegex.test(part)
        ? `(?:<ruby><rb>${part}</rb><rt>[^<]+</rt></ruby>|${part})`
        : `(?:<ruby><rb>[^<]+</rb><rt>${part}</rt></ruby>|${part})`
    })
    const regex = new RegExp(regexParts.join('(?:\\s*?)'), 'g');

    ['.SentKanji', '.SentFurigana'].forEach(selector => {
      document.querySelectorAll(selector).forEach((el, i) => {
        const type = getType(i)
        if (el.querySelector('b, i, u, span, strong') || type !== '例') return
        el.innerHTML = el.innerHTML
          .replace(regex, match => `<strong>${match}</strong>`)
          .replace(/[～〜]/g, `<strong>${cleanWord()}</strong>`)
      })
    })
  }

  function highlightWords() {
    document.querySelectorAll('.SentFurigana').forEach((el, i) => {
      if (el.querySelector('b, i, u, span')) return
      const type = getType(i)
      const prefix = type.match(/^(関|対)/)
      if (!prefix) return
      const tag = prefix[1] === '関' ? 'synonym' : 'antonym'
      const content = el.innerHTML
        .replace(/^<em[^>]*>［[^]*?］<\/em>/, '')
        .trim()
        .replace(/^［[^]*?］/, '')
      el.innerHTML = el.querySelector('em') ? `<em lang='ja'>［${type}］</em><span class='${tag}'>${content}</span>` : `<span class='${tag}'>${content}</span>`
    })
  }

  function showHint() {
    if (isBackSide()) {
      document.querySelectorAll('a.hint').forEach(hint => hint.style.display = 'none')
    }
  }

  function setAnkiWebAudio() {
    if (!isAnkiWeb()) return

    document.querySelectorAll('.VocabAudio, .SentAudio').forEach(el => {
      const audio = el.querySelector('audio')
      if (!audio) return
      audio.removeAttribute('controls')
      el.insertAdjacentHTML('beforeend', '<a class="replay-button soundLink"><svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="29"/><path d="M56.502,32.301l-37.502,20.101l0.329,-40.804l37.173,20.703Z"/></svg></a>')
      el.querySelector('.replay-button').addEventListener('click', e => {
        e.preventDefault()
        document.querySelectorAll('audio').forEach(a => a !== audio && !a.paused && a.pause())
        audio.currentTime = 0
        audio.play()
      })
    })
  }

  function setEdgeTTS() {
    const { enable, hotkey, domain, params } = CONFIG().settings.tts
    if (enable === 'never') return

    const getVoice = () => {
      const voices = params.voiceName.split(',').map(v => v.trim())
      return voices.length === 1 ? voices[0] : voices[Math.floor(Math.random() * voices.length)]
    }
    const getSentKanji = (index) => {
      const SentKanji = [
        '{{text:kanji:SentKanji1}}',
        '{{text:kanji:SentKanji2}}',
        '{{text:kanji:SentKanji3}}',
        '{{text:kanji:SentKanji4}}'
      ][index] || ''
      return SentKanji.replace(/・/g, '、')
    }
    document.querySelectorAll('.SentAudio').forEach((el, i) => {
      if (enable === 'fallback' && el.childNodes.length) return
      const text = getSentKanji(i)
      if (!text) return
      const queryString = new URLSearchParams({ ...params, text, voiceName: getVoice() })
      const audio = document.createElement('audio')
      audio.preload = 'none'
      domain.forEach(url => {
        const source = document.createElement('source')
        source.src = `${url}api/aiyue?${queryString}`
        source.type = 'audio/mpeg'
        audio.appendChild(source)
      })
      el.appendChild(audio)
      el.insertAdjacentHTML(
        'beforeend',
        '<a class="tts replay-button soundLink"><svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="29"/><path d="M56.502,32.301l-37.502,20.101l0.329,-40.804l37.173,20.703Z"/></svg></a>',
      )
      el.querySelector('.tts.replay-button').addEventListener('click', e => {
        e.preventDefault()
        document.querySelectorAll('audio').forEach(a => a !== audio && !a.paused && a.pause())
        audio.currentTime = 0
        audio.play()
      })
    })
    triggerAudioPlayback(hotkey)
  }

  function triggerAudioPlayback(hotkey) {
    if (!hotkey) return

    let currentAudioIndex = 0
    let isError = false
    document.addEventListener('keydown', e => {
      if (e.key.toLowerCase() === hotkey.toLowerCase()) {
        const audios = document.querySelectorAll('.CardSide audio')
        audios.forEach((audio) => {
          audio.pause()
          audio.currentTime = 0
        })
        if (!isError) currentAudioIndex = 0
        playNext(audios)
      }
    })
    function playNext(audios) {
      if (currentAudioIndex >= audios.length) return
      const audio = audios[currentAudioIndex]
      audio.play().then(() => {
        audio.onended = () => {
          currentAudioIndex++
          playNext(audios)
        }
        isError = false
      }).catch(() => (isError = true))
    }
  }

  function autoCopyWord() {
    if (!CONFIG().settings.autoCopy[getDevice()]) return
    setTimeout(async () => {
      try {
        await navigator.clipboard.writeText(cleanWord())
      } catch (err) {
        console.error(err)
      }
    }, 0)
  }

  function setupCard() {
    lookUp()
    feedback()
    setLang()
    setType()
    markWords()
    hideKanji()
    removeSpaces()
    checkVersion()
  }
</script>

<script>
  setupCard()
  hideFurigana()
  setAnkiWebAudio()
</script>

{{/Alt1}}